package jmine.tec.hydra.util.monitor;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;

import jmine.tec.utils.Tuple;
import jmine.tec.utils.register.Receiver;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

import bancosys.tec.rtm.impl.RtmController;

/**
 * Responsavel por monitorar alterações em diretórios.
 * 
 * @author piercio
 */
public class DirectoryMonitor implements Receiver<Tuple<String, String>>, InitializingBean, DisposableBean {

    private static final int DEFAULT_INTERVAL = 5000;

    private static final Logger LOG = Logger.getLogger(DirectoryMonitor.class);

    private final Timer timer = new Timer(true);

    private final Map<SystemDirectory, Long> sysdirs = new HashMap<SystemDirectory, Long>();

    private DirectoryListener listener;

    private final long pollingInterval;

    private RtmController rtmController;

    /**
     * Constructor.
     */
    public DirectoryMonitor() {
        this.pollingInterval = DEFAULT_INTERVAL;
    }

    /**
     * Constructor.
     * 
     * @param pollingInterval intervalo de tempo entre observações do diretório.
     */
    public DirectoryMonitor(long pollingInterval) {
        if (pollingInterval < 0) {
            throw new IllegalArgumentException("The polling interval must be positive.");
        }
        this.pollingInterval = pollingInterval;
    }

    /**
     * @return o valor do campo listener
     */
    public DirectoryListener getListener() {
        return this.listener;
    }

    /**
     * @param listener o valor para o campo listener
     */
    public void setListener(DirectoryListener listener) {
        this.listener = listener;
    }

    /**
     * @param rtmController o valor para o campo rtmController
     */
    public void setRtmController(RtmController rtmController) {
        this.rtmController = rtmController;
    }

    /**
     * Registra os diretórios com seus respectivos sistemas.
     * 
     * @param registers uma lista de Tuplas de SystemName/DirectoryName
     */
    public void register(Collection<? extends Tuple<String, String>> registers) {
        if (registers != null) {
            for (Tuple<String, String> sysdir : registers) {
                this.addDirectory(sysdir.getHead(), sysdir.getTail());
            }
        }
    }

    /**
     * Adiciona um diretório a monitoração.
     * 
     * @param systemName o nome do sistema associado a esse diretório.
     * @param directoryName o nome do diretório.
     */
    private void addDirectory(String systemName, String directoryName) {
        File directory = new File(directoryName);
        if (!directory.exists() || !directory.isDirectory()) {
            throw new IllegalArgumentException("File argument must denote an existing directory. systemName: " + systemName
                    + "; directoryName: " + directoryName);
        }
        SystemDirectory sysdir = new SystemDirectory(systemName, directory);
        this.sysdirs.put(sysdir, new Long(directory.lastModified()));
    }

    /**
     * {@inheritDoc}
     */
    public void afterPropertiesSet() throws Exception {
        this.start();
    }

    /**
     * Inicia o monitoramento.
     */
    public void start() {
        this.timer.schedule(new FileMonitorNotifier(), 0, this.pollingInterval);
    }

    /**
     * Finaliza a execução do monitoramento.
     */
    public void stop() {
        this.timer.cancel();
    }

    /**
     * Atualiza o horário da última modificação do diretório.
     * 
     * @param sysdir sistema/diretório.
     * @param newModified horário da modificação.
     */
    private void updateLastModification(SystemDirectory sysdir, long newModified) {
        this.sysdirs.put(sysdir, newModified);
    }

    /**
     * Notificador responsável por executar os {@FileListeners} quando alguma alteração for detectada em um diretório.
     * 
     * @author piercio
     */
    private class FileMonitorNotifier extends TimerTask {

        /**
         * Verifica, a cada intervalo, se houve alguma alteração em algum diretório monitorado, e avisa os listeners correspondentes.
         */
        @Override
        public void run() {
            for (Entry<SystemDirectory, Long> entry : DirectoryMonitor.this.sysdirs.entrySet()) {
                SystemDirectory sysDir = entry.getKey();
                this.checkDirectory(sysDir, sysDir.getDirectory(), entry.getValue());
            }
        }

        /**
         * Verifica se diretório foi modificado.
         * 
         * @param sysdir sistema/diretório.
         * @param directoryFile {@link File} representando o diretório.
         * @param lastModified última alteração do diretório.
         */
        private void checkDirectory(SystemDirectory sysdir, File directoryFile, long lastModified) {
            LOG.debug("Checking directory " + sysdir.getDirectory() + " for system " + sysdir.getSystem());
            long newModified = directoryFile.exists() ? directoryFile.lastModified() : -1;
            if (newModified != lastModified) {
                this.directoryModified(sysdir, directoryFile, newModified);
            }
        }

        /**
         * Diretório foi modificado.
         * 
         * @param sysdir sistema/diretório.
         * @param directoryFile {@link File} representando o diretório.
         * @param newModified novo horário da última alteração do diretório.
         */
        private void directoryModified(SystemDirectory sysdir, File directoryFile, long newModified) {
            LOG.debug("... Directory " + sysdir.getDirectory() + " modified!");
            DirectoryMonitor.this.updateLastModification(sysdir, newModified);
            try {
                DirectoryMonitor.this.listener.directoryChanged(sysdir);
            } catch (Throwable t) {
                LOG.error("Erro escutando diretório " + directoryFile.getAbsolutePath(), t);
                try {
                    if (DirectoryMonitor.this.rtmController != null) {
                        DirectoryMonitor.this.rtmController.monitor(t);
                    }
                } catch (Throwable t2) {
                    LOG.error("Erro ao logar erro no RTM.", t2);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void destroy() throws Exception {
        this.timer.cancel();
    }
}
